package jmine.tec.services.api;

import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jmine.tec.di.scope.TypedScope;
import jmine.tec.services.ServicesController;
import jmine.tec.services.impl.ServicesImplMessages;
import jmine.tec.utils.uppercase.UpperCaseValueMap;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.LockMode;
import org.hibernate.Session;
import org.hibernate.proxy.HibernateProxy;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.controller.PersistenceEnabledController;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.dao.BeanNotFoundException;

/**
 * Entidade responsavel por armazenar os parametros utilizados por um servico.
 * 
 * @author Rafael Volpato (Jun 4, 2007)
 */
public class ServiceProperties implements TypedScope, RawInput {

    private static Log log = LogFactory.getLog(ServiceProperties.class);

    private ServiceProperties parent;

    private final Map<String, Object> properties;

    private final PersistenceEnabledController controller;

    /**
     * Construtor de um ServiceProperties "pai".
     * 
     * @param controller controller
     */
    public ServiceProperties(ServicesController controller) {
        this(controller, null, controller.instantiateMap());
    }

    /**
     * Construtor de um ServiceProperties "filho" sem valores inicias atribuidos.
     * 
     * @param controller controller
     * @param parent serviceProperties
     */
    public ServiceProperties(ServicesController controller, ServiceProperties parent) {
        this(controller, parent, controller.instantiateMap());
    }

    /**
     * Construtor de um ServiceProperties "filho" com valores iniciais atribuidos.
     * 
     * @param controller controller
     * @param parent serviceProperties
     * @param properties mapa com as propriedades iniciais
     */
    public ServiceProperties(PersistenceEnabledController controller, ServiceProperties parent, Map<String, Object> properties) {
        this.controller = controller;
        this.parent = parent;
        this.properties = properties;
    }

    /**
     * Retorna todas as propriedades desse serviceProperties.
     * 
     * @return todas as propriedades desse serviceProperties
     */
    public Map<String, Object> getProperties() {
        return this.properties;
    }

    /**
     * @param parent servicePropertie
     */
    public void setParent(ServiceProperties parent) {
        this.parent = parent;
    }

    /**
     * @return a ServicePropertie
     */
    public ServiceProperties getParent() {
        return this.parent;
    }

    /**
     * Retorna o valor original informado (sem tratamento de uppercase).
     * 
     * @param key chave da propriedade
     * @return o valor original informado
     */
    @SuppressWarnings("unchecked")
    public String getRaw(String key) {
        String value = null;

        // Obter o valor do propertie local
        if (this.properties instanceof UpperCaseValueMap) {
            value = (String) ((UpperCaseValueMap) this.properties).getRaw(key);
        } else {
            value = (String) this.properties.get(key);
        }

        // Buscar o valor no parent, se necessario.
        if (value == null && this.parent != null) {
            value = this.parent.getRaw(key);
        }

        return value;
    }

    /**
     * Retorna o valor armazenado para a chave passada como parametro.
     * 
     * @param <T> Tipo do objeto de retorno esperado
     * @param key chave da propriedade
     * @param clazz classe do objeto de retorno esperado
     * @return valor armazenado para a chave
     */
    @SuppressWarnings("unchecked")
    public <T> T get(String key, Class<T> clazz) {
        T propertyValue = null;

        /* Obter o objeto que esta armazenado no mapa */
        Object mapObj = this.properties.get(key);

        /*
         * Se o objeto encontrado no mapa é uma variavel, retornar o seu valor se ele foi definido.
         */
        if (mapObj != null && mapObj instanceof String) {
            String varName = ((String) mapObj).trim();
            if (varName.startsWith("$")) {
                T propertyRef = this.get(varName, clazz);
                if (propertyRef != null) {
                    mapObj = propertyRef;
                }
            }
        }

        /* Se o valor da propriedade for "vazio", retornal null */
        if (mapObj != null && "".equals(mapObj)) {
            mapObj = null;
        }

        /* Verifica se o objeto é da classe esperada */
        if (clazz.isInstance(mapObj)) {
            propertyValue = (T) mapObj;
        } else if (mapObj != null) {
            /*
             * Se ja existia um objeto associado a chave passada e estavam procurando pela chave (String) retornar a chave
             */
            if (clazz == String.class) {
                propertyValue = (T) key;
            } else {
                throw new ServiceValidationExecutionException(ServicesImplMessages.UNEXPECTED_OBJECT_TYPE.create(clazz.getCanonicalName(),
                        mapObj.getClass().getCanonicalName()));
            }
        }

        /* Se a propriedade for um persistable, pegar o objeto atualizado. */
        if (propertyValue != null && Persistable.class.isInstance(propertyValue)) {
            propertyValue = (T) this.reloadPersistable((Persistable) propertyValue);
        }

        /* Se a propriedade for uma lista, percorrer essa lista e fazer o reload de todos os persistables dentro dela */
        if (propertyValue != null && List.class.isInstance(propertyValue)) {
            List list = (List) propertyValue;
            for (int i = 0; i < list.size(); i++) {
                Object element = list.get(i);
                if (Persistable.class.isInstance(element)) {
                    list.set(i, this.reloadPersistable((Persistable) element));
                }
            }
        }

        /* A propriedade não foi definida, verificar se a propriedade não foi definida no parent */
        if (propertyValue == null && this.parent != null) {
            propertyValue = this.parent.get(key, clazz);

        }
        return propertyValue;
    }

    /**
     * Caso seja um objeto persistente, recarrega.
     * 
     * @param originalObject Objeto original
     * @return Objeto recarregado.
     * @param <T> tipo do objeto que vai ser retornado
     */
    @SuppressWarnings("unchecked")
    public <T extends Persistable> T reloadPersistable(T originalObject) {
        T obj = originalObject;
        try {
            Serializable pk = null;
            Session session = this.getController().getSessionFactory().getCurrentSession();
            Class<? extends T> objClass = (Class<? extends T>) obj.getClass();

            if (obj instanceof HibernateProxy) {
                session.lock(obj, LockMode.READ);
                objClass = ((HibernateProxy) obj).getHibernateLazyInitializer().getPersistentClass();
            }
            pk = obj.getPk();

            // [rafael.volpato, julien.renaut] O fato da pk ser igual a null, quer dizer que o objeto eh TRANSIENT, sendo assim nao existe
            // necessidade de tentar obter uma versao mais atualizada do objeto na base de dados porque simplesmente nao vai existir ;-)
            if (pk != null) {
                BaseDAO<T> dao = this.getController().getDAOFactory().getGenericDAO(objClass);
                obj = dao.findByPk(pk);

                if (obj instanceof HibernateProxy) {
                    obj = (T) ((HibernateProxy) obj).getHibernateLazyInitializer().getImplementation();
                }
            }
        } catch (BeanNotFoundException e) {
            log.warn("Objeto não existe mais no banco de dados! Provavelmente ja foi removido.");

        } catch (Exception e) {
            // Ignora e usa o obj original
            log.error("Erro desconhecido! Não foi possivel recuperar o objeto atualizado na base de dados", e);
        }
        return obj;
    }

    /**
     * Guarda uma propriedade.
     * 
     * @param key chave da propriedade
     * @param value valor da propriedade
     */
    public void put(String key, Object value) {
        Object localValue = value;

        if (localValue instanceof String && "".equals(localValue)) {
            localValue = null;
        }

        this.properties.put(key, localValue);
    }

    /**
     * Guarda uma propriedade que vai ser compartilhada entre servicos. Se existir uma serviceProperties "pai", essa propriedade vai ser
     * armazenada no "pai".
     * 
     * @param key chave da propriedade
     * @param value valor da propriedade
     */
    public void putDefinition(String key, Object value) {
        Object localValue = value;

        if (localValue instanceof String && "".equals(localValue)) {
            localValue = null;
        }

        if (this.parent != null) {
            this.parent.putDefinition(key, localValue);
        } else {
            // Não gravar instancias de objetos que não sejam "reais", se o objeto for "lazy" obter uma instancia real.
            if (value instanceof HibernateProxy) {
                localValue = this.reloadPersistable((Persistable) localValue);
            }
            this.properties.put(key, localValue);
        }
    }

    /**
     * Verifica se a propriedade de nome <code>key</code> foi definida (mesmo se com valor <code>null</code>).
     * 
     * @param key chave da propriedade
     * @return <code>true</code> se a chave foi definida, <code>false</code> caso contrario.
     */
    public boolean containsKey(String key) {
        boolean contains = this.properties.containsKey(key);

        if (!contains && this.parent != null) {
            contains = this.parent.containsKey(key);
        }

        return contains;
    }

    /**
     * Verifica se a propriedade de nome <code>key</code> foi definida e possui um valor valido (diferente de <code>null</code>).
     * 
     * @param key chave da propriedade
     * @return <code>true</code> se a chave foi definida e possui um valor valido, <code>false</code> caso contrario.
     */
    public boolean hasProperty(String key) {
        boolean contains = this.properties.containsKey(key) && this.properties.get(key) != null;

        if (!contains && this.parent != null) {
            contains = this.parent.hasProperty(key);
        }

        return contains;
    }

    /**
     * @return the controller
     */
    private PersistenceEnabledController getController() {
        return this.controller;
    }

    /**
     * Retorna instancia de ServiceProperties corrente na forma de String.
     * 
     * @return a instancia de ServiceProperties corrente na forma de String
     */
    @Override
    public String toString() {
        String token = "";
        StringBuilder sb = new StringBuilder();

        sb.append("{ ");

        Set<String> keys = this.properties.keySet();
        for (String key : keys) {
            if ("".equals(key.trim())) {
                continue;
            }

            Object value = this.properties.get(key);

            if (value != null) {
                sb.append(token);
                sb.append(key);
                sb.append("='");

                if (value instanceof String) {
                    sb.append(value);
                } else {
                    sb.append("<");
                    sb.append(value.getClass().getCanonicalName());
                    sb.append(">");
                }

                sb.append("'");
                token = ", ";
            }
        }

        sb.append("} ");

        return sb.toString();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ServiceProperties)) {
            return false;
        }
        if (obj == this) {
            return true;
        }
        ServiceProperties o = (ServiceProperties) obj;
        return new EqualsBuilder().append(this.parent, o.parent).append(this.properties, o.properties).isEquals();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(this.parent).append(this.properties).toHashCode();
    }
}
